import os
import atexit
import threading
import time
from typing import Optional
from playwright.sync_api import sync_playwright
from pathlib import Path
import platform
from typing import Dict, List, Tuple, Optional
from playwright.sync_api import sync_playwright, Route, Request
from playwright.sync_api import Page, Response
from playwright.sync_api import sync_playwright
PROFILE_DIR = str(Path(__file__).resolve().parent / ".pw-data" / "douyin-profile")

print(PROFILE_DIR)

is_linux = platform.system() == "Linux"
# print(PROFILE_DIR)
PLAYWRIGHT_HEADLESS = 1
# 全局常量，放在文件顶部
DETAIL_URL = "https://www.douyin.com/aweme/v1/web/aweme/detail/"


def build_params(aweme_id: str) -> dict:
    # 只放“相对固定”的字段，动态签名由页面 JS 注入
    return {
        "device_platform": "webapp",
        "aid": "6383",
        "channel": "channel_pc_web",
        "aweme_id": aweme_id,
        "version_code": "190500",
        "browser_name": "edge",
        "os_name": "Windows",
        "referer": "https://www.douyin.com/",

    }

    # 拦截
    #  先清一次，避免重复挂老的 handler

    # 3) 在 context 层挂拦截（只拦“大资源”，不要拦脚本/XHR/文档）


_HEAVY_TYPES = {"image", "media", "font"}
_BLOCK_HOSTS = {
    "mon.zijieapi.com",  # 监控/上报
    "mcs.zijieapi.com",
}
_ALLOW_SUFFIXES = (
    "douyin.com", "douyinstatic.com", "bytescm.com", "bytetos.com", "bytednsdoc.com"
)


# -------- 拦截策略（精简资源以“轻跑”页面 JS）--------
BLOCK_RESOURCE_TYPES = {"image", "media", "font", "stylesheet", "other",
                        "mon.zijieapi.com",  # 监控/埋点 batch
                        "mcs.zijieapi.com",  # webid/list 反复上报
                        "privacy.zijieapi.com",  # CMP/隐私同意
                        "lf-security.bytegoofy.com",  # 安全/指纹 SDK
                        "lf-rc1.yhgfb-cn-static.com"  # 验证码中心（如未触发尽量不拉）
                        }  # 直接中止这些类型
ALLOW_HOST_SUFFIXES = (".douyin.com", 'https://www.douyin.com/aweme/v1/web/aweme/detail/'
                       ,
                       "douyin.com",  # 主站：文档 + detail XHR
                       "douyinstatic.com",  # 前端框架/播放器分片
                       "bytednsdoc.com",  # 有时静态资源会走这里
                       "bytescm.com", "bytetos.com"  # 字节系静态 CDN（框架/播放器）
                       )  # 只放行抖音主域；其余域名默认拦截（可按需扩展）

SAFE_BLOCK_TYPES = {"image", "media", "font", "stylesheet", "other"}  # 渲染大头，直接拦
BLOCK_HOSTS = {
    "mon.zijieapi.com",  # 监控/埋点 batch
    "mcs.zijieapi.com",  # webid/list 反复上报
    "privacy.zijieapi.com",  # CMP/隐私同意
    "lf-security.bytegoofy.com",  # 安全/指纹 SDK
    "lf-rc1.yhgfb-cn-static.com"  # 验证码中心（如未触发尽量不拉）
}
ALLOW_HOST_SUFFIXES = (
    "douyin.com",  # 主站：文档 + detail XHR
    "douyinstatic.com",  # 前端框架/播放器分片
    "bytednsdoc.com",  # 有时静态资源会走这里
    "bytescm.com", "bytetos.com"  # 字节系静态 CDN（框架/播放器）
)


# BLOCK_RESOURCE_TYPES = {}


def route_handler(route: Route, request: Request):
    """
    路由拦截规则：
    - 必须允许页面的 HTML 与关键 JS/XHR 执行（否则无法生成 a_bogus）；
    - 尽可能中止图片/媒体/字体/样式等重资源；
    - 仅放行目标主域，第三方域名直接拦截，减少体量与指纹噪音。
    """
    url = request.url
    host = (request.url.split("/")[2]).split(":")[0] if "://" in url else ""
    rtype = request.resource_type

    # 1) detail 必须放行（最关键）
    if "/aweme/v1/web/aweme/detail" in url:
        return route.continue_()

    # 2) 按类型拦大头
    if rtype in SAFE_BLOCK_TYPES:
        return route.abort()

    # 3) 样式表可“空响应”加速 onload（避免长等）
    if rtype == "stylesheet":
        return route.fulfill(status=200, body="/* noop */", content_type="text/css")

    # 4) 按域名拦监控/隐私/指纹等（不影响拿 detail）
    if host in BLOCK_HOSTS:
        return route.abort()

    # 5) 白名单域名正常放行（主站 + 核心静态）
    if host.endswith(ALLOW_HOST_SUFFIXES):
        return route.continue_()

    # if '.js' in url:
    #     return route.abort()

    # 6) 其他第三方：保守放行一次；若想更狠可改成 abort()
    return route.continue_()
def _ctx_route(route, request):
    rtype = request.resource_type
    url = request.url
    host = ""
    try:
        host = url.split("/")[2].split(":")[0]
    except Exception:
        pass

    # 1) 关键接口/主域名放行
    if any(host.endswith(sfx) for sfx in _ALLOW_SUFFIXES):
        # 但“大资源”仍可拦
        if rtype in _HEAVY_TYPES:
            return route.abort()
        return route.continue_()

    # 2) 监控/上报等三方域名拦掉
    if host in _BLOCK_HOSTS:
        return route.abort()

    # 3) 其余第三方保守放行（需要更狠就改成 abort）
    if rtype in _HEAVY_TYPES:
        return route.abort()
    return route.continue_()


class DouyinBrowser:
    """懒加载 + 全局单例 + 自动自愈 + 常驻首页 Page"""
    _instance: Optional["DouyinBrowser"] = None
    _glock = threading.Lock()

    def __init__(self):
        self._lock = threading.Lock()
        self._pw = None
        self._browser = None
        self._context = None
        self._page = None  # ⭐ 常驻 page

        hv = os.environ.get("PLAYWRIGHT_HEADLESS", "1").lower()
        self._headless = hv in ("", "1", "true", "yes", "on")
        self._proxy = os.environ.get("PLAYWRIGHT_PROXY", "").strip()
        atexit.register(self.close)

    @classmethod
    def instance(cls) -> "DouyinBrowser":
        if cls._instance is not None:
            return cls._instance
        with cls._glock:
            if cls._instance is None:
                cls._instance = cls()
        return cls._instance

    def _ensure_started(self):
        with self._lock:
            # 如果已有 page，先探活
            if self._page:
                try:
                    # 尝试访问一下，证明 page 还活着
                    print('已有page 尝试轻量探活：', end='')
                    print(f'成功，{self._page.title()}')
                    return  # 成功就直接返回，不重启
                except Exception:
                    print('探活失败 重启')
                    # 如果失败，说明挂了 → 彻底清理
                    self._safe_close()
            else:
                print('无page')
            # 到这里说明没 page 或者挂了 → 重启浏览器
            print("[INIT] 启动 Playwright 浏览器...")
            if not self._pw:
                self._pw = sync_playwright().start()

            linux_args = [
                "--disable-gpu",
                "--disable-software-rasterizer",
                "--disable-dev-shm-usage",  # /dev/shm 小时很关键
                "--no-first-run",
                "--no-default-browser-check",
                "--force-color-profile=srgb",
                "--disable-features=TranslateUI,BlinkGenPropertyTrees",
                "--autoplay-policy=no-user-gesture-required",
                "--no-zygote",
                "--single-process",  # 若崩溃频繁可临时加；稳定后可去掉
            ]

            args = [
                "--disable-gpu",
                "--disable-software-rasterizer",
                "--no-first-run",
                "--no-default-browser-check",
                "--force-color-profile=srgb",
            ]

            if is_linux:
                args = linux_args
            flg = False
            if is_linux:
                flg = True
            ua = (
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/140.0.0.0 Safari/537.36"
            )
            if is_linux:

                ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339 Safari/537.36"

            print(ua)

            launch_kwargs = {
                "user_data_dir": PROFILE_DIR,  # 👈 在这里指定目录
                "headless": flg,  # True=无头，False=有头
                # "channel": "chrome",  # Windows 上推荐指定
                "args": args,
                "locale": "zh-CN",
                "timezone_id": "Asia/Shanghai",
                "user_agent": ua,
                "viewport": {"width": 1280, "height": 800},
            }



            # 🚀 直接返回的是 Context
            self._context = self._pw.chromium.launch_persistent_context(**launch_kwargs)

            # 拦截
            # self._context.route('**/*', route_handler)

            self._browser = self._context.browser
            self._context.set_default_timeout(15000)

            # self._context.set_extra_http_headers({
            #     "User-Agent": ua,
            #     # "Accept-Language": "zh-CN,zh;q=0.9",
            #     # "Sec-CH-UA": f'"Chromium";v="{major}", "Not_A Brand";v="99"',
            #     # "Sec-CH-UA-Platform": '"Linux"' if is_linux else '"Windows"',
            #     # "Sec-CH-UA-Mobile": "?0",
            #     # "Upgrade-Insecure-Requests": "1",
            # })

            # ⭐ 打开首页并保持
            self._page = self._context.new_page()
            print('开主页')

            self._page.goto("https://www.douyin.com/", wait_until="domcontentloaded")
            # print('标签',self._page.title())
            # print('html',self._page.content())
            print('url', self._page.url)

            # time.sleep(2)

            while 1:
                try:
                    tle = self._page.title()
                    if tle:
                        print('标签', tle)
                        break
                except Exception:
                    print('没拿到标签,再试，拿不到一直试')

    @property
    def browser(self):
        self._ensure_started()
        return self._browser

    @property
    def context(self):
        self._ensure_started()
        return self._context

    def get_page(self):
        """返回常驻首页 page"""
        self._ensure_started()
        return self._page

    def new_page(self):
        """需要额外 page 时再开新的"""
        self._ensure_started()
        return self._context.new_page()

    def _safe_close(self):
        try:
            if self._page:
                self._page.close()
        except Exception:
            pass
        try:
            if self._context:
                self._context.close()
        except Exception:
            pass
        try:
            if self._browser:
                self._browser.close()
        except Exception:
            pass
        self._page = None
        self._context = None
        self._browser = None

    def close(self):
        try:
            self._safe_close()
        finally:
            try:
                if self._pw:
                    self._pw.stop()
            except Exception:
                pass
            self._pw = None

        # 在全局page.py 里的 DouyinBrowser 类里加上：

    def fetch_detail(self, aweme_id: str, retries: int = 3, warmup: bool = True) -> dict:
        """
        在“常驻首页 page”里发起 detail 请求，返回 JSON。
        - 自动在视频页预热（让 cookie/签名就绪）
        - JS 端不盲目 parse，返回 ctype/text 便于排错
        """
        page = self.get_page()

        # # 1) 先去对应视频页“点火”，通常更稳定
        # if warmup:
        #     try:
        #         page.goto(f"https://www.douyin.com/video/{aweme_id}",
        #                   wait_until="domcontentloaded", timeout=15000)
        #         # 轻等 300ms，给前端 SDK 注入/签名预留时间
        #         page.wait_for_timeout(300)
        #     except Exception:
        #         # 有时候首屏没打 detail，滚动一下再说
        #         try:
        #             page.mouse.wheel(0, 1200)
        #             page.wait_for_timeout(200)
        #         except Exception:
        #             pass

        # 2) 更稳健的 JS（不直接 JSON.parse；总是保留原始 text 片段）
        js = """
        async (input) => {
          try {
            const qs = new URLSearchParams(input.params).toString();
            const url = input.url + "?" + qs;
            const r = await fetch(url, {
              credentials: "include",
              headers: {
                "Accept": "application/json, text/plain, */*",
                "Referer": "https://www.douyin.com/"
              }
            });
            const ctype = (r.headers.get("content-type") || "").toLowerCase();
            const txt = await r.text();
            let data = null, jsonError = null;
            if (ctype.includes("application/json")) {
              try { data = JSON.parse(txt); } catch (e) { jsonError = String(e); }
            }
            return {
              ok: r.ok, status: r.status, url: r.url,
              ctype, text: txt ? txt.slice(0, 5000) : "",
              hasData: !!data, data, jsonError
            };
          } catch (e) {
            return { ok: false, error: String(e) };
          }
        }
        """

        params = {"url": DETAIL_URL, "params": build_params(aweme_id)}

        last = None
        for i in range(retries):
            res = page.evaluate(js, params)
            last = res
            # 成功拿到 JSON
            if res.get("hasData") and isinstance(res.get("data"), dict):
                data = res["data"]
                # 抖音惯例：需要有 aweme_detail 才算真正成功
                if data.get("aweme_detail") or data.get("status_code") == 0:
                    return data
            # 否则小等/滚动再试一次
            try:
                page.mouse.wheel(0, 800)
            except Exception:
                pass
            page.wait_for_timeout(300)

        # 3) 仍失败，给出尽可能多的上下文
        raise RuntimeError(
            "❌ fetch_detail 失败：\n"
            f"  aweme_id: {aweme_id}\n"
            f"  ok={last.get('ok')} status={last.get('status')} ctype={last.get('ctype')}\n"
            f"  jsonError={last.get('jsonError')} error={last.get('error')}\n"
            f"  url={last.get('url')}\n"
            f"  text_prefix={repr((last.get('text') or '')[:200])}"
        )


# 全局单例
douyin_browser = DouyinBrowser.instance()
